package com.ncr.travel.tas.shared.configuration;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.ncr.travel.tas.shared.configuration.annotations.Configuration;

/**
 * Utility class used to inject configuration values dynamically into a class
 *
 * @author Jon Cordero
 *
 */
public class ConfigurationInjector {

	private static final Logger logger = LoggerFactory.getLogger(ConfigurationInjector.class);

	private Validator validator;

	public ConfigurationInjector() {
		// initialize default validator
		ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
		this.validator = factory.getValidator();
	}

	public ConfigurationInjector(Validator validator) {
		this.validator = validator;
	}

	/**
	 * Gets a list of all configuration members (based on the
	 * {@link Configuration} annotation) on the given object
	 *
	 * @param object
	 * @return
	 */
	public List<String> getConfigurationNames(Object object) {

		List<String> configurationNames = new ArrayList<String>();

		// search all fields
		Field[] fields = object.getClass().getDeclaredFields();
		for (Field field : fields) {

			Configuration configurationAnnotation = field.getAnnotation(Configuration.class);
			if (configurationAnnotation == null)
				continue; // this is not a configuration field

			// else this IS a configuration field

			configurationNames.add(configurationAnnotation.key());
		}

		// TODO: search methods?

		return configurationNames;
	}

	/**
	 * Sets a configuration value on an object
	 *
	 * @param object
	 *            The object to set the configuration on
	 * @param configurationName
	 * @param configurationValue
	 */
	public void setConfiguration(Object object, String configurationName, Object configurationValue) {
		// try to set it as a field
		Field[] fields = object.getClass().getDeclaredFields();
		for (Field field : fields) {
			Configuration configurationAnnotation = field.getAnnotation(Configuration.class);
			if (configurationAnnotation != null && configurationAnnotation.key().equals(configurationName)) {
				// found the member, set it

				try {

					//turn off default security checking.
					field.setAccessible(true);

					field.set(object, configurationValue);

				} catch (Exception e) {
					logger.error("Unable to set configuration member named (" + configurationName + ") on class: " + object.getClass().getName(), e);
					throw new RuntimeException("Unable to set configuration value for configuration (" + configurationName + ") on class: " + object.getClass().getName(), e);
				}
				return;
			}
		}

		// else didn't find an applicable field
		// try method
		// TODO:

		// else couldn't find a matching member under that name
		logger.error("Could not find the configuration (" + configurationName + ") on the class: " + object.getClass().getName());
		throw new RuntimeException("Could not find the configuration (" + configurationName + ") on the class: " + object.getClass().getName());
	}

	/**
	 * Gets a configuration value on an object
	 *
	 * @param object
	 *            The object to get the configuration on
	 * @param configurationName
	 * @throws IllegalAccessException
	 * @throws IllegalArgumentException
	 */
	public Object getValueByConfiguration(Object object, String configurationName) throws IllegalArgumentException, IllegalAccessException {

		// get list of fields
		Field[] fields = object.getClass().getDeclaredFields();

		//find the field we are interested in
		for (Field field : fields) {

			//turn off default security checking.
			field.setAccessible(true);

			Configuration configurationAnnotation = field.getAnnotation(Configuration.class);
			if (configurationAnnotation != null && configurationAnnotation.key().equals(configurationName)) {
				// found the member, return it's value
				return field.get(object);
			}
		}

		// else didn't find an applicable field
		// try method
		// TODO:

		// else couldn't find a matching member under that name
		logger.error("Could not find the field value for (" + configurationName + ") on the class: " + object.getClass().getName());
		throw new RuntimeException("Could not find the configuration (" + configurationName + ") on the class: " + object.getClass().getName());
	}

	/**
	 * Gets an @Configuration by name on an object
	 *
	 * @param object
	 *            The object to get the configuration on
	 * @param configurationName
	 * @throws IllegalAccessException
	 * @throws IllegalArgumentException
	 */
	public Configuration getConfiguration(Object object, String configurationName) throws IllegalArgumentException, IllegalAccessException {

		// get list of fields
		Field[] fields = object.getClass().getDeclaredFields();

		//find the field we are interested in
		for (Field field : fields) {

			Configuration configurationAnnotation = field.getAnnotation(Configuration.class);
			if (configurationAnnotation != null && configurationAnnotation.key().equals(configurationName)) {
				// found it
				return configurationAnnotation;
			}
		}

		// else didn't find an applicable field
		// try method
		// TODO:

		// else couldn't find a matching member under that name
		logger.error("Could not find the field value for (" + configurationName + ") on the class: " + object.getClass().getName());
		throw new RuntimeException("Could not find the configuration (" + configurationName + ") on the class: " + object.getClass().getName());
	}

	/**
	 * Validates a configuration value on an object
	 *
	 * @param object
	 */
	public void validateConfiguration(Object object) {
		Set<ConstraintViolation<Object>> violations = this.validator.validate(object);
		if (violations.size() > 0) {
			String violationString = "";
			String newLine = System.getProperty("line.separator");

			// log specific violations
			for (ConstraintViolation<Object> violation : violations) {
				violationString += newLine + "Violation: " + violation.getMessage();
			}

			logger.error("Invalid values on object: " + object.getClass().getName() + violationString);
			throw new RuntimeException("Invalid values on object: " + object.getClass().getName() + violationString);
		}

	}
}
